﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading;
using ThePresenter.Net.PresenterCom.Actions;
using ThePresenter.Net.PresenterCom.Uids;
using ThePresenter.Properties;


namespace ThePresenter.Net.NetCom
{
	public class NetComServer : IDisposable // TODO Rename to Server
	{
		#region Fields

		#region Global fields

		private DebugClass _D;

		private Guid _ServerGuid;

		#endregion

		#region Listening fields

		private Socket _ListeningSocket;
		private int _BacklogNumber = 10;
		private IPEndPoint _LocalEndPoint;

		#endregion

		#region Sending fields

		private ClientManager _Clients;

		private Thread _BroadcastingThread;
		private Boolean _EndBroadcasting;
		private Dictionary<Socket, Byte[]> _BroadcastingInfo;

		#endregion
	
		#endregion


		#region Properties

		#region Clients management properties

		public Boolean HasClients
		{
			get { return this._Clients.Count > 0; }
		}

		#endregion

		#endregion


		#region Events
		///// <summary>
		///// IncomingConnection event handler delegate 
		///// </summary>
		///// <param name="sender">Server that received connection</param>
		///// <param name="S">Socket of new connection</param>
		//public delegate void IncomingConnectionHandler(object sender, Socket S);

		///// <summary>
		///// Event fired when new connection is established
		///// </summary>
		//public event IncomingConnectionHandler IncomingConnection;

		/// <summary>
		/// IncomingAction event handler delegate 
		/// </summary>
		/// <param name="sender">client that sent action</param>
		/// <param name="action">action received</param>
		public delegate void IncomingClientActionHandler(Uid sender, PresenterAction action);

		/// <summary>
		/// Event fired when new data is received
		/// </summary>
		public event IncomingClientActionHandler IncomingAction;
		
		/// <summary>
		///Delegate for events when client was added or removed 
		/// </summary>
		/// <param name="sender">Server that managed client</param>
		/// <param name="client">ClientInfo of managed client</param>
		public delegate void ClientManagedHandler(object sender, Uid client);

		/// <summary>
		/// Event fired when new client is added to list of clients
		/// </summary>
		public event ClientManagedHandler ClientAdded;

		/// <summary>
		/// Event fired when new client is removed from list of clients
		/// </summary>
		public event ClientManagedHandler ClientRemoved;

		/// <summary>
		/// Event fired when client info is filled up
		/// </summary>
		public event ClientManagedHandler ClientCompleted;
		#endregion


		#region Constructors

		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="Port">Port to listen on</param>
		/// <param name="ServerGuid">Guid of server</param>
		public NetComServer(int Port, Guid ServerGuid)
		{
			_D = new DebugClass(Resources.Server);

			this._ServerGuid = ServerGuid;
			this._BroadcastingInfo = this.GetBroadcastingInfo();

			NetworkChange.NetworkAvailabilityChanged +=new NetworkAvailabilityChangedEventHandler(NetworkChange_NetworkAvailabilityChanged);
			NetworkChange.NetworkAddressChanged += new NetworkAddressChangedEventHandler(NetworkChange_NetworkAddressChanged);

			this.BeginBroadcast(Port);
		}

		#endregion


		#region Methods

		#region Clients methods

		/// <summary>
		/// Gets list of clients curently connected to server
		/// </summary>
		/// <returns>Array of clients</returns>
		public ClientInfo[] GetClients()
		{
			return this._Clients.ToArray();
		}

		public ClientInfo GetClientById(Uid Id)
		{
			return this._Clients[Id].Info;
		}

		#endregion

		#region Listening methods

		/// <summary>
		/// Start listening for incoming connections from clients
		/// </summary>
		/// <exception cref="System.ArgumentNullException"></exception>
		/// <exception cref="System.ArgumentOutOfRangeException"></exception>
		/// <exception cref="System.InvalidOperationException"></exception>
		/// <exception cref="System.NotSupportedException"></exception>
		/// <exception cref="System.ObjectDisposedException"></exception>
		/// <exception cref="System.Security.SecurityException"></exception>
		/// <exception cref="System.Net.Sockets.SocketException"></exception>
		public void BeginListen()
		{
			this.EndListen();
			
			this._ListeningSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

			//TRY_BIND:
			//try
			//{
				this._ListeningSocket.Bind(this._LocalEndPoint);
			//}
			//catch (SocketException ex)
			//{
			//    if (ex.SocketErrorCode == SocketError.AddressAlreadyInUse)
			//    {
			//        this._LocalEndPoint.Port++;
			//        goto TRY_BIND;
			//    }
			//}

			this._ListeningSocket.Listen(this._BacklogNumber);

			this._ListeningSocket.BeginAccept(this.OnClientConnect, null);
			_D.Write(Resources.WaitingForConnections);
		}

		/// <summary>
		/// End listening for connections
		/// </summary>
		/// <exception cref="System.ObjectDisposedException"></exception>
		/// <exception cref="System.Net.Sockets.SocketException"></exception>
		public void EndListen()
		{
			if (this._ListeningSocket != null)
			{
				this._ListeningSocket.Shutdown(SocketShutdown.Both);
				this._ListeningSocket.Close();
			}
		}

		/// <summary>
		/// Callback method for BeginAccept() method 
		/// </summary>
		/// <param name="Result">Accept result</param>
		private void OnClientConnect(IAsyncResult Result)
		{
			try
			{
				// Get new socket for incoming connection
				Socket AcceptedSocket = this._ListeningSocket.EndAccept(Result);
				_D.Write(Resources.ReceivedNewSocketFrom, AcceptedSocket.RemoteEndPoint.ToString());

				// Add socket to clients
				Client C = new Client(AcceptedSocket);
				this._Clients.Add(C);

				if (this.ClientAdded != null) this.ClientAdded(this, C.LocalId);

				C.IncomingData += new Client.ClientIncomingDataHandler(Client_IncomingData);
				C.Start();

				_D.Write(Resources.WaitingForDataFrom, AcceptedSocket.RemoteEndPoint.ToString());

				// Fire IncomingConnection event
				// if (this.IncomingConnection != null) IncomingConnection(this, AcceptedSocket);
			}
			catch (Exception E)
			{
				_D.Write("NetComServer.OnClientConnect()\nThere was error handling incoming connection: {0}", E.Message);
			}
			finally
			{
				try
				{
					// Begin listening for more
					this._ListeningSocket.BeginAccept(this.OnClientConnect, null);
					_D.Write(Resources.WaitingForConnections);
				}
				catch (Exception E)
				{
					DebugClass.ShowMessage("NetComServer.OnClientConnect()\nThere was error trying to accept new connections: {0}", E.Message);
				}
			}
		}

		void Client_OnDisconnected(Uid sender)
		{
			// this._Clients.Remove(sender);
			if (this.ClientRemoved != null) this.ClientRemoved(this, sender);
		}

		void Client_IncomingData(object sender, byte[] Data)
		{
			Client Sender = sender as Client;

			PresenterAction action;

			using (MemoryStream MS = new MemoryStream(Data))
			{
				BinaryFormatter BF = new BinaryFormatter();
				action = (PresenterAction)BF.Deserialize(MS);
			}

			if (action is IntroductionAction) // Update client info
			{
				((IntroductionAction)action).Execute(Sender);
				if (ClientCompleted != null && Sender.IsComplete)
				{
					ClientCompleted(this, Sender.LocalId);
				}
			}

			if (action is DisconnectingAction) // Close client and fire event
			{
				this.Client_OnDisconnected(((DisconnectingAction)action).Uid);
			}

			if (IncomingAction != null)
				IncomingAction(Sender.LocalId, action);
		}
		#endregion

		#region Send methods
		/// <summary>
		/// Sends data to all connected clients
		/// </summary>
		/// <param name="Data">Data to send</param>
		/// <exception cref="ThePresenter.Net.NetCom.ServerNotBroadcastingException">If server is not broadcasting</exception>
		/// <exception cref="System.Runtime.Serialization.SerializationException">If action could not be serialized</exception>
		public void Send(PresenterCom.Actions.PresenterAction A)
		{
			this.SendTo(A, (Uid[])null);
		}

		/// <summary>
		/// Sends data to single targeted endpoint
		/// </summary>
		/// <param name="Data">Data to send</param>
		/// <param name="Recipient">Target to send data to</param>
		/// <exception cref="ThePresenter.Net.NetCom.ServerNotBroadcastingException">If server is not broadcasting</exception>
		/// <exception cref="System.Runtime.Serialization.SerializationException">If action could not be serialized</exception>
		public void SendTo(PresenterCom.Actions.PresenterAction A, Uid Recipient)
		{
			this.SendTo(A, new Uid[] { Recipient });
		}

		/// <summary>
		/// Send data to multiple targeted endpoints
		/// </summary>
		/// <param name="Data">Data to send</param>
		/// <param name="Recipients">Targets to send data to</param>
		/// <exception cref="ThePresenter.Net.NetCom.ServerNotBroadcastingException">If server is not broadcasting</exception>
		/// <exception cref="System.Runtime.Serialization.SerializationException">If action could not be serialized</exception>
		public void SendTo(PresenterCom.Actions.PresenterAction A, Uid[] Recipients)
		{
			if (this.HasClients)
			{
				byte[] ActionData = A.Serialize();

				if (Recipients == null || Recipients.Length == 0)
				{
					this._Clients.Send(ActionData);
				}
				else if (Recipients.Length == 1)
				{
					this._Clients.Send(ActionData, Recipients[0]);
				}
				else
				{
					this._Clients.Send(ActionData, Recipients);
				}

				_D.Write("Have {0} bytes to send", ActionData.Length);
			}
			else
			{
				throw new ServerNotBroadcastingException("Server is not broadcasting. Could not send data.");
			}
		}
		#endregion

		#region Broadcast methods

		/// <summary>
		/// Begins the broadcast.
		/// </summary>
		private void BeginBroadcast(int Port)
		{
			// Receiving values init
			this._LocalEndPoint = new IPEndPoint(IPAddress.Any, Port);

			// Sending values init
			this._Clients = new ClientManager();
			this._Clients.OnClientDisconnected += new Client.ClientDisconnectedEventHandler(Client_OnDisconnected);

			// Advertising thread init
			this._EndBroadcasting = false;
			this._BroadcastingThread = new Thread(new ThreadStart(BroadcastAdvertisement));
			this._BroadcastingThread.Name = "BroadcastingThread";
			this._BroadcastingThread.Start();
		}
	
		/// <summary>
		/// Retrieves informations required for advertising broadcast
		/// </summary>
		/// <returns>Dictionary of sockets and bytes with appropriate message</returns>
		/// <exception cref="BroadcastingSocketException">If broadcasting socket cannot be created</exception>
		private Dictionary<Socket, Byte[]> GetBroadcastingInfo()
		{
			Dictionary<Socket, Byte[]> Info = new Dictionary<Socket, Byte[]>();

			MemoryStream MS;

			IPEndPoint[] IPEPs = NetComTools.GetLocalEndPoints(ThePresenter.Properties.Settings.Default.DefaultApplicationPort);

			if (IPEPs.Length == 0) throw new NoSuitableInterfacesException();

			foreach (IPEndPoint EP in IPEPs)
			{
				MS = new MemoryStream();
				try
				{
					Socket S = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
					S.EnableBroadcast = true;
					S.Bind(EP);

					ServerInfo SI = new ServerInfo(this._ServerGuid, (Settings.Default.Username == String.Empty ? System.Environment.MachineName : Settings.Default.Username), (IPEndPoint)S.LocalEndPoint); // NOTE: System.Environment.MachineName by nemalo byt potrebne, ale pre istotu
					NetComTools.BinaryFormatter.Serialize(MS, SI);
					Info.Add(S, MS.ToArray());
				}
				catch (SocketException)
				{
					throw new BroadcastingSocketException();
				}
				finally
				{
					MS.Close();
				}
			}

			return Info;
		}

		/// <summary>
		/// Advertise server to valid interfaces
		/// </summary>
		private void BroadcastAdvertisement()
		{
			IPEndPoint BroadcastEP = new IPEndPoint(IPAddress.Broadcast, ThePresenter.Properties.Settings.Default.DefaultApplicationPort);

			while (!this._EndBroadcasting)
			{
				try
				{
					// _D.Write("Sending advertisement to all ({0}) connected networks.", this._BroadcastingInfo.Count);
					lock (this._BroadcastingInfo)
					{
						// TODO: lock list, if changed while broadcasting
						// mozno zrusit to mazanie Remove() dole?
						foreach (KeyValuePair<Socket, Byte[]> Info in this._BroadcastingInfo)
						{
							try
							{
								Info.Key.SendTo(Info.Value, SocketFlags.None, BroadcastEP);
								// _D.Write("Sent advertisement");
							}
							catch (Exception E)
							{
								// this._BroadcastingInfo.Remove(Info.Key);
								_D.Write("NetComServer.BroadcastAdvertisement()\nCould not send advertisement(target removed): {0}", E.Message);

								if (this._BroadcastingInfo.Count == 0)
								{
									throw new NoSuitableInterfacesException(); // TODO: nejako toto odchytit (spustene v inom threade, mozno budu problemy...)
								}
							}
						}
					}

					Thread.Sleep(ThePresenter.Properties.Settings.Default.ServerBeaconTime);
				}
				catch (ThreadInterruptedException) { }; // When killing thread exception is thrown
			}
		}

		void NetworkChange_NetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
		{
			_D.Write("NetworkAvailabilityChanged");
			NetComTools.ForceUpdateLocalIPAddresses();
			this.UnbindInfos();
			this._BroadcastingInfo = this.GetBroadcastingInfo();
		}

		void NetworkChange_NetworkAddressChanged(object sender, EventArgs e)
		{
			_D.Write("NetworkAddressChanged");
			NetComTools.ForceUpdateLocalIPAddresses();
			this.UnbindInfos();
			this._BroadcastingInfo = this.GetBroadcastingInfo();
		}

		private void UnbindInfos()
		{
			foreach (Socket S in this._BroadcastingInfo.Keys)
			{
				S.Shutdown(SocketShutdown.Both);
				S.Close();
			}
		}

		#endregion

		#endregion


		#region IDisposable Members
		/// <summary>
		/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
		/// </summary>
		public void Dispose()
		{
			this._Clients.Dispose();

			this._EndBroadcasting = true;
			this._BroadcastingThread.Interrupt();
		}
		#endregion
	}
}
